/** @file

  A PEIM with the following responsibilities:

  - verify & configure the Q35 TSEG in the entry point,
  - provide SMRAM access by producing PEI_SMM_ACCESS_PPI

  This PEIM runs from RAM, so we can write to variables with static storage
  duration.

  Copyright (C) 2013, 2015, Red Hat, Inc.<BR>
  Copyright (c) 2010 - 2024, Intel Corporation. All rights reserved.<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/IoLib.h>
#include <Library/PcdLib.h>
#include <Library/PciLib.h>
#include <Library/PeiServicesLib.h>
#include <Ppi/SmmAccess.h>

#include <OvmfPlatforms.h>

#include "SmramInternal.h"

//
// PEI_SMM_ACCESS_PPI implementation.
//

/**
  Opens the SMRAM area to be accessible by a PEIM driver.

  This function "opens" SMRAM so that it is visible while not inside of SMM.
  The function should return EFI_UNSUPPORTED if the hardware does not support
  hiding of SMRAM. The function should return EFI_DEVICE_ERROR if the SMRAM
  configuration is locked.

  @param  PeiServices            General purpose services available to every
                                 PEIM.
  @param  This                   The pointer to the SMM Access Interface.
  @param  DescriptorIndex        The region of SMRAM to Open.

  @retval EFI_SUCCESS            The region was successfully opened.
  @retval EFI_DEVICE_ERROR       The region could not be opened because locked
                                 by chipset.
  @retval EFI_INVALID_PARAMETER  The descriptor index was out of bounds.

**/
STATIC
EFI_STATUS
EFIAPI
SmmAccessPeiOpen (
  IN EFI_PEI_SERVICES    **PeiServices,
  IN PEI_SMM_ACCESS_PPI  *This,
  IN UINTN               DescriptorIndex
  )
{
  EFI_HOB_GUID_TYPE               *GuidHob;
  EFI_SMRAM_HOB_DESCRIPTOR_BLOCK  *DescriptorBlock;

  //
  // Get the number of regions in the system that can be usable for SMRAM
  //
  GuidHob         = GetFirstGuidHob (&gEfiSmmSmramMemoryGuid);
  DescriptorBlock = GET_GUID_HOB_DATA (GuidHob);
  ASSERT (DescriptorBlock);

  if (DescriptorIndex >= DescriptorBlock->NumberOfSmmReservedRegions) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // According to current practice, DescriptorIndex is not considered at all,
  // beyond validating it.
  //
  return SmramAccessOpen (&This->LockState, &This->OpenState);
}

/**
  Inhibits access to the SMRAM.

  This function "closes" SMRAM so that it is not visible while outside of SMM.
  The function should return EFI_UNSUPPORTED if the hardware does not support
  hiding of SMRAM.

  @param  PeiServices              General purpose services available to every
                                   PEIM.
  @param  This                     The pointer to the SMM Access Interface.
  @param  DescriptorIndex          The region of SMRAM to Close.

  @retval EFI_SUCCESS              The region was successfully closed.
  @retval EFI_DEVICE_ERROR         The region could not be closed because
                                   locked by chipset.
  @retval EFI_INVALID_PARAMETER    The descriptor index was out of bounds.

**/
STATIC
EFI_STATUS
EFIAPI
SmmAccessPeiClose (
  IN EFI_PEI_SERVICES    **PeiServices,
  IN PEI_SMM_ACCESS_PPI  *This,
  IN UINTN               DescriptorIndex
  )
{
  EFI_HOB_GUID_TYPE               *GuidHob;
  EFI_SMRAM_HOB_DESCRIPTOR_BLOCK  *DescriptorBlock;

  //
  // Get the number of regions in the system that can be usable for SMRAM
  //
  GuidHob         = GetFirstGuidHob (&gEfiSmmSmramMemoryGuid);
  DescriptorBlock = GET_GUID_HOB_DATA (GuidHob);
  ASSERT (DescriptorBlock);

  if (DescriptorIndex >= DescriptorBlock->NumberOfSmmReservedRegions) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // According to current practice, DescriptorIndex is not considered at all,
  // beyond validating it.
  //
  return SmramAccessClose (&This->LockState, &This->OpenState);
}

/**
  Inhibits access to the SMRAM.

  This function prohibits access to the SMRAM region.  This function is usually
  implemented such that it is a write-once operation.

  @param  PeiServices              General purpose services available to every
                                   PEIM.
  @param  This                     The pointer to the SMM Access Interface.
  @param  DescriptorIndex          The region of SMRAM to Close.

  @retval EFI_SUCCESS            The region was successfully locked.
  @retval EFI_DEVICE_ERROR       The region could not be locked because at
                                 least one range is still open.
  @retval EFI_INVALID_PARAMETER  The descriptor index was out of bounds.

**/
STATIC
EFI_STATUS
EFIAPI
SmmAccessPeiLock (
  IN EFI_PEI_SERVICES    **PeiServices,
  IN PEI_SMM_ACCESS_PPI  *This,
  IN UINTN               DescriptorIndex
  )
{
  EFI_HOB_GUID_TYPE               *GuidHob;
  EFI_SMRAM_HOB_DESCRIPTOR_BLOCK  *DescriptorBlock;

  //
  // Get the number of regions in the system that can be usable for SMRAM
  //
  GuidHob         = GetFirstGuidHob (&gEfiSmmSmramMemoryGuid);
  DescriptorBlock = GET_GUID_HOB_DATA (GuidHob);
  ASSERT (DescriptorBlock);

  if (DescriptorIndex >= DescriptorBlock->NumberOfSmmReservedRegions) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // According to current practice, DescriptorIndex is not considered at all,
  // beyond validating it.
  //
  return SmramAccessLock (&This->LockState, &This->OpenState);
}

/**
  Queries the memory controller for the possible regions that will support
  SMRAM.

  @param  PeiServices           General purpose services available to every
                                PEIM.
  @param This                   The pointer to the SmmAccessPpi Interface.
  @param SmramMapSize           The pointer to the variable containing size of
                                the buffer to contain the description
                                information.
  @param SmramMap               The buffer containing the data describing the
                                Smram region descriptors.

  @retval EFI_BUFFER_TOO_SMALL  The user did not provide a sufficient buffer.
  @retval EFI_SUCCESS           The user provided a sufficiently-sized buffer.

**/
STATIC
EFI_STATUS
EFIAPI
SmmAccessPeiGetCapabilities (
  IN EFI_PEI_SERVICES          **PeiServices,
  IN PEI_SMM_ACCESS_PPI        *This,
  IN OUT UINTN                 *SmramMapSize,
  IN OUT EFI_SMRAM_DESCRIPTOR  *SmramMap
  )
{
  return SmramAccessGetCapabilities (
           SmramMapSize,
           SmramMap
           );
}

//
// LockState and OpenState will be filled in by the entry point.
//
STATIC PEI_SMM_ACCESS_PPI  mAccess = {
  &SmmAccessPeiOpen,
  &SmmAccessPeiClose,
  &SmmAccessPeiLock,
  &SmmAccessPeiGetCapabilities
};

STATIC EFI_PEI_PPI_DESCRIPTOR  mPpiList[] = {
  {
    EFI_PEI_PPI_DESCRIPTOR_PPI | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST,
    &gPeiSmmAccessPpiGuid, &mAccess
  }
};

//
// Utility functions.
//
STATIC
UINT8
CmosRead8 (
  IN UINT8  Index
  )
{
  IoWrite8 (0x70, Index);
  return IoRead8 (0x71);
}

STATIC
UINT32
GetSystemMemorySizeBelow4gb (
  VOID
  )
{
  UINT32  Cmos0x34;
  UINT32  Cmos0x35;

  Cmos0x34 = CmosRead8 (0x34);
  Cmos0x35 = CmosRead8 (0x35);

  return ((Cmos0x35 << 8 | Cmos0x34) << 16) + SIZE_16MB;
}

//
// Entry point of this driver.
//
EFI_STATUS
EFIAPI
SmmAccessPeiEntryPoint (
  IN       EFI_PEI_FILE_HANDLE  FileHandle,
  IN CONST EFI_PEI_SERVICES     **PeiServices
  )
{
  UINT16  HostBridgeDevId;
  UINT8   EsmramcVal;
  UINT8   RegMask8;
  UINT32  TopOfLowRam, TopOfLowRamMb;

  //
  // This module should only be included if SMRAM support is required.
  //
  ASSERT (FeaturePcdGet (PcdSmmSmramRequire));

  //
  // Verify if we're running on a Q35 machine type.
  //
  HostBridgeDevId = PciRead16 (OVMF_HOSTBRIDGE_DID);
  if (HostBridgeDevId != INTEL_Q35_MCH_DEVICE_ID) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: no SMRAM with host bridge DID=0x%04x; only "
      "DID=0x%04x (Q35) is supported\n",
      __func__,
      HostBridgeDevId,
      INTEL_Q35_MCH_DEVICE_ID
      ));
    goto WrongConfig;
  }

  //
  // Confirm if QEMU supports SMRAM.
  //
  // With no support for it, the ESMRAMC (Extended System Management RAM
  // Control) register reads as zero. If there is support, the cache-enable
  // bits are hard-coded as 1 by QEMU.
  //
  EsmramcVal = PciRead8 (DRAMC_REGISTER_Q35 (MCH_ESMRAMC));
  RegMask8   = MCH_ESMRAMC_SM_CACHE | MCH_ESMRAMC_SM_L1 | MCH_ESMRAMC_SM_L2;
  if ((EsmramcVal & RegMask8) != RegMask8) {
    DEBUG ((
      DEBUG_ERROR,
      "%a: this Q35 implementation lacks SMRAM\n",
      __func__
      ));
    goto WrongConfig;
  }

  TopOfLowRam = GetSystemMemorySizeBelow4gb ();
  ASSERT ((TopOfLowRam & (SIZE_1MB - 1)) == 0);
  TopOfLowRamMb = TopOfLowRam >> 20;

  //
  // Some of the following registers are no-ops for QEMU at the moment, but it
  // is recommended to set them correctly, since the ESMRAMC that we ultimately
  // care about is in the same set of registers.
  //
  // First, we disable the integrated VGA, and set both the GTT Graphics Memory
  // Size and the Graphics Mode Select memory pre-allocation fields to zero.
  // This takes just one write to the Graphics Control Register.
  //
  PciWrite16 (DRAMC_REGISTER_Q35 (MCH_GGC), MCH_GGC_IVD);

  //
  // Set Top of Low Usable DRAM.
  //
  PciWrite16 (
    DRAMC_REGISTER_Q35 (MCH_TOLUD),
    (UINT16)(TopOfLowRamMb << MCH_TOLUD_MB_SHIFT)
    );

  //
  // Given the zero graphics memory sizes configured above, set the
  // graphics-related stolen memory bases to the same as TOLUD.
  //
  PciWrite32 (
    DRAMC_REGISTER_Q35 (MCH_GBSM),
    TopOfLowRamMb << MCH_GBSM_MB_SHIFT
    );
  PciWrite32 (
    DRAMC_REGISTER_Q35 (MCH_BGSM),
    TopOfLowRamMb << MCH_BGSM_MB_SHIFT
    );

  //
  // Set TSEG Memory Base.
  //
  InitQ35TsegMbytes ();
  PciWrite32 (
    DRAMC_REGISTER_Q35 (MCH_TSEGMB),
    (TopOfLowRamMb - mQ35TsegMbytes) << MCH_TSEGMB_MB_SHIFT
    );

  //
  // Set TSEG size, and disable TSEG visibility outside of SMM. Note that the
  // T_EN bit has inverse meaning; when T_EN is set, then TSEG visibility is
  // *restricted* to SMM.
  //
  EsmramcVal &= ~(UINT32)MCH_ESMRAMC_TSEG_MASK;
  EsmramcVal |= mQ35TsegMbytes == 8 ? MCH_ESMRAMC_TSEG_8MB :
                mQ35TsegMbytes == 2 ? MCH_ESMRAMC_TSEG_2MB :
                mQ35TsegMbytes == 1 ? MCH_ESMRAMC_TSEG_1MB :
                MCH_ESMRAMC_TSEG_EXT;
  EsmramcVal |= MCH_ESMRAMC_T_EN;
  PciWrite8 (DRAMC_REGISTER_Q35 (MCH_ESMRAMC), EsmramcVal);

  //
  // TSEG should be closed (see above), but unlocked, initially. Set G_SMRAME
  // (Global SMRAM Enable) too, as both D_LCK and T_EN depend on it.
  //
  PciAndThenOr8 (
    DRAMC_REGISTER_Q35 (MCH_SMRAM),
    (UINT8)((~(UINT32)MCH_SMRAM_D_LCK) & 0xff),
    MCH_SMRAM_G_SMRAME
    );

  GetStates (&mAccess.LockState, &mAccess.OpenState);

  //
  // SmramAccessLock() depends on "mQ35SmramAtDefaultSmbase"; init the latter
  // just before exposing the former via PEI_SMM_ACCESS_PPI.Lock().
  //
  InitQ35SmramAtDefaultSmbase ();

  //
  // We're done. The next step should succeed, but even if it fails, we can't
  // roll back the above BuildGuidHob() allocation, because PEI doesn't support
  // releasing memory.
  //
  return PeiServicesInstallPpi (mPpiList);

WrongConfig:
  //
  // We really don't want to continue in this case.
  //
  ASSERT (FALSE);
  CpuDeadLoop ();
  return EFI_UNSUPPORTED;
}
